CNSS Recruit 2019 Crypto Writeup

坤坤的代码〇

实质上是模意义下斐波那契数列求和,注意到模数很小容易出现循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
from functools import reduce
from operator import add
f = [0xf, 0xf]
loop_len = 0
n = 1 << 64 | 2
for i in range(2, n):
f.append((f[i - 1] + f[i - 2]) % 0x10)
if f[i] == 0xf and f[i-1] == 0xf:
loop_len = i - 1
break
ans = (n // loop_len) * reduce(add, f[:-2]) + reduce(add, f[:n % loop_len])
print("cnss{%d}" % ans)
#cnss{159871781972149447364}

坤坤的代码Ⅰ

$\sum_{i|n}^{n\in[1,10^7]}1=\sum_{i=1}^{10^7}\lfloor\frac{10^7}{i}\rfloor$

1
2
3
4
5
6
n = 10000000
ans = 0
for i in range(1, n + 1):
ans += n // i
print("cnss{%d}" % ans)
#cnss{162725364}

坤坤的代码Ⅱ

使用 MillerRabin 算法重写 is_prime 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from random import randint

def MillerRabin(n):
a = randint(2,n-2)
s = 0
d = n - 1
while (d & 1) == 0:
s += 1
d >>= 1

x = pow(a, d, n)
for _ in range(s):
newX = pow(x, 2, n)
if newX == 1 and x != 1 and x != n - 1:
return False
x = newX

return x == 1

def is_Prime(n):
if ((n & 1) == 0 or n % 5 == 0):
return False
for _ in range(100):
if MillerRabin(n) == False:
return False
return True

def next_prime(n):
while True:
if is_Prime(n):
return n
n += 1

seed = 0x5eed
for _ in range(64):
seed <<= 16
seed = next_prime(seed)

print("cnss{%s}" % (hex(seed)[-32:]))
#cnss{004302cd0023009100990725007903d7}

坤坤的代码Ⅲ

求满足 $i\in[1,n],i^{\varphi(n)}\equiv1(\mod n),\gcd(i,n)=1,\forall j\in[1,\varphi(n)),i^j\not\equiv1(\mod n)$ 的 $i$ 之和。

根据欧拉定理:$\gcd(i,n)=1\Rightarrow i^{\varphi(n)}\equiv1(\mod n)$.

实际上,$67108934=2\times33554467$ ,故很容易排除 $\gcd(i,n)\neq1$ 的 $i$.

那只要再排除 $\exist j\in[1,\varphi(n)),i^j\equiv1(\mod n)$ 的 $i$ 即可;

注意到 $\varphi(n)=33554466=2\times3^3\times11\times56489$ 且对于一个 $i$, $\forall d>1,d|\varphi(n), \exist j=\frac{\varphi(n)}{d}\in[1,\varphi(n)),(i^d)^j\equiv1(\mod n)$ ,则可以排除 $i^d$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
n = 67108934
pn = 33554466 #phi(n)
ps = [56489, 11, 621379, 3, 169467, 33, 1864137, 9, 508401, 99, 5592411, 27, 1525203, 297, 16777233, 2, 112978, 22, 1242758, 6, 338934, 66, 3728274, 18, 1016802, 198, 11184822, 54, 3050406, 594, 33554466] #factors of phi(n)
'''
for i2 in range(2):
for i3 in range(4):
for i11 in range(2):
for i56489 in range(2):
ps.append((2**i2) * (3**i3) * (11**i11) * (56489**i56489))
ps.remove(0)
'''
gg = [False for i in range(n)]
for i in range(1,n):
if i%2==1 and i!=33554467:
for j in ps:
gg[pow(i,j,n)]=True
cnt = 0
for i in range(1,n):
if i%2==1 and i!=33554467:
if not gg[i]:
cnt+=1
print("cnss{%d}" % cnt)
#cnss{341217052646350}

坤坤的代码Ⅳ

注意到题目要求的式子$\sum_{b\in\text{product}(a,2^n)}^{Q|\sum b_i}\prod b_i$可由两个 $\text{repeat}=2^n$ 合并为 $\text{repeat}=2^{n+1}$ ,整体的思路就是求出 $\text{repeat}=2^n$ 时的值,对 $\text{repeat}\neq2^n$ 的情况分解为多个二次幂之和求解,此做法是ⅠⅡⅢ的通解。

问题在于如何合并。首先原式是低效的枚举判断形式,为了高效找出符合 $Q|\sum b_i$ 的 $b_i$ 就要考虑按 $\sum b_i \mod Q$ 分类 以便合并;其次先求积再求和使得必须枚举 ,不如先求和再求积。


$$
f_{n,r}=\sum_{b\in \text{product}(a,2^{n})}^{\sum b_i\equiv r(\mod Q) }\prod b_i
$$
则有
$$
f_{n+1,r}=\sum_{i=0}^{Q-1}f_{n,i}f_{n,Q-i(\mod i)}
$$
$n$ 不等的 $f$ 也是一样方法合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
P = 0x7fffffff
Q = 0x100
n = 0x100
a = [pow(i, i**i, P) for i in range(n)]
f = [[0 for i in range(Q)] for i in range(32)]
for i in a:
f[0][i % Q] += i
f[0][i % Q] %= P * Q
for lv in range(1, 32):
for i in range(Q):
for j in range(Q):
f[lv][(i + j) % Q] += f[lv - 1][i] * f[lv - 1][j]
f[lv][(i + j) % Q] %= P * Q
print("cnss{%d}" % f[3][0])
#cnss{56955670368}
print("cnss{%d}" % f[16][0])
#cnss{480814021633}
ans = [[0 for i in range(Q)] for i in range(32)]
ans[1] = f[0]
for lv in range(2, 32):
for i in range(Q):
for j in range(Q):
ans[lv][(i + j) % Q] += f[lv - 1][i] * ans[lv - 1][j]
ans[lv][(i + j) % Q] %= P * Q

print("cnss{%d}" % ans[31][0])
#cnss{66559571170}

坤坤的帅气签名

使用 CRT 加速 RSA 签名时, $c$ 通过 $c_p\equiv m^d(\mod p),c_q\equiv m^d(\mod q)$ 合并得到。

不妨假设在计算 $c_p$ 时出现错误得到一个错误值 $c_p’\neq c_p$ 而 $c_q$ 是正确的。

记由 $c_p’,c_q$ 合并出来的值为 $c’$,则有
$$
q=\gcd((c’^e-m)\mod n,n)
$$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from math import gcd
from binascii import a2b_hex
from gmpy2 import invert as inv

n = 109453943855659271832842942560403561756870609158731037775662385351197002786101553313741128891698293686816823489928849957259082253354725368827785717195435302172225437710056999830798824021337641459232307818433049338256971969203037801338219292565591596823843084807425759066355695095315215256626194447457000935219
e = 65537
m = 664571392881790422435277591416285461838362142565
c_ = 95361947547578027398844873615910853429249196840875210666696459318107820034497276956275479240249952230477194176576310896173689631161740588202298964125311372712579942498844950844776941548440922576913828770721063505802129955338545026791678869888011537973251863068824639014310112791103793602257959620977303959166
c = 106039992459918542481959218676031242448622622921702794062900589891335070401325885060562630102179872636335524488857155980500818588740868617342364258438580858663620359722304271036542646651953697809289409672464136670237497042598885667755826964760657089501369425580341586708889011946540651753137711555709077414486
q = gcd(pow(c_, e, n)-m, n)
p = n // q

d = inv(e, (p - 1) * (q - 1))
m = pow(c, d, n)
s = str(a2b_hex(hex(m)[2:]), encoding="utf8")

print(s)
#cnss{Y0u_h4ve_1earn3d_Fau1t_Att4ck}

Classical

首先枚举 key_size , 计算按当前 key_size 将 cipher_text 分割,用 hamming_distance 量化各个 key_size 的可能性,从而得出最可能的 key_size. 之后将按确定的 key_size 分组的 cipher_text 每组相应位置分别取出串联成重组串,在用频率攻击解决 singlechar_xor 问题即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from base64 import b64decode
from binascii import b2a_hex
from itertools import combinations

CHARACTER_FREQ ={
'a': 0.0651738, 'b': 0.0124248, 'c': 0.0217339, 'd': 0.0349835, 'e': 0.1041442, 'f': 0.0197881, 'g': 0.0158610,
'h': 0.0492888, 'i': 0.0558094, 'j': 0.0009033, 'k': 0.0050529, 'l': 0.0331490, 'm': 0.0202124, 'n': 0.0564513,
'o': 0.0596302, 'p': 0.0137645, 'q': 0.0008606, 'r': 0.0497563, 's': 0.0515760, 't': 0.0729357, 'u': 0.0225134,
'v': 0.0082903, 'w': 0.0171272, 'x': 0.0013692, 'y': 0.0145984, 'z': 0.0007836, ' ': 0.1918182
}

def repeating_key_xor(plaintext, key):
ciphertext = b''
i = 0
for byte in plaintext:
ciphertext += bytes([byte ^ key[i]])
i = i + 1 if i < len(key) - 1 else 0
return ciphertext

def get_english_score(input_bytes):
score = 0
for byte in input_bytes:
score += CHARACTER_FREQ.get(chr(byte).lower(), 0)
return score

def singlechar_xor(input_bytes, key_value):
output = b''
for char in input_bytes:
output += bytes([char ^ key_value])
return output

def singlechar_xor_brute_force(ciphertext):
candidates = []
for key_candidate in range(256):
plaintext_candidate = singlechar_xor(ciphertext, key_candidate)
candidate_score = get_english_score(plaintext_candidate)
result = {
'key': key_candidate,
'score': candidate_score,
'plaintext': plaintext_candidate
}
candidates.append(result)
return sorted(candidates, key=lambda c: c['score'], reverse=True)[0]

def hamming_distance(binary_seq_1, binary_seq_2):
assert len(binary_seq_1) == len(binary_seq_2)
dist = 0
for bit1, bit2 in zip(binary_seq_1, binary_seq_2):
diff = bit1 ^ bit2
dist += sum([1 for bit in bin(diff) if bit == '1'])
return dist

def break_repeating_key_xor(binary_data):
normalized_distances = {}
for key_size in range(2, 41):
chunks = [binary_data[i:i + key_size] for i in range(0, len(binary_data), key_size)][:4]
distance = 0
pairs = combinations(chunks, 2)
for (x, y) in pairs:
distance += hamming_distance(x, y)
distance /= 6
normalized_distance = distance / key_size
normalized_distances[key_size] = normalized_distance
possible_key_sizes = sorted(normalized_distances, key=normalized_distances.get)[:3]
possible_plaintexts = []

for d in possible_key_sizes:
key = b''
for i in range(d):
block = b''
for j in range(i, len(binary_data), d):
block += bytes([binary_data[j]])
key += bytes([singlechar_xor_brute_force(block)['key']])
possible_plaintexts.append((repeating_key_xor(binary_data, key), key))
return max(possible_plaintexts, key=lambda k: get_english_score(k[0]))

def write_text():
cipher = b'GxBHXgcWBhc6QgB4VhAXAAEUAx4AdgcIRwYMHAJ/XRo0E1UaDRcUERkLbSoIQQ8JUgwsERU+BFUDCgBRQh4AMkkTWw8LUg06Q1MzHwUdQlJGBwheHw9HQAAKBUU9VFMoHhwaAF4UFQQcdh0PVgBFGgAtEREtExQdEQEUAx4Adg0SXVUsFEU3UBotBVUMAFJDCx4AJUVHUQIEEQ5/RhotEwZOAgBbFUwKOEkPVhxFGgA+VV0WVh0PExcUEQkAOEkVXB0AAUU7UB4+BR5JAV4UEAkBdggJV04SGgwrVF8dAwFOCx0UERkGPkkVXB0AAUUsVBZ/P1UHC1JcBx5FNQECVgUWSSQxVVM2GFUdCh9RQhwAJA8SXgsWUgwsEQc3EwcLRR9bEAlFMgwLWgkNBjE3UB1/HxtOERpRQg4XMwgTW04RGgQrERUtGRhOCAsUDwUWIhsCQB1FAAA6WgBxP1UCCgRRQhgKdgECUhxFGgAtEQAvExQFSVJNBxhFIQwLX04sUg4xXgQLHhQaRR9BEQUGdgEGRwZFE0U5UAF/GxocAFJEDgkEJQAJVE4WHRAxVUgWVhIcBBxAQiVFOAwRVhxFAQQoERJ/ERoKARdHEUwCOVIqSk4IGxYrQxYsBVlOEhpRDEwWPgxHRA8JGRZzEQctExQKFlJbDEwRPgxHVBwKBws7CzIxElUXAAYYQg4cdgECUhgAHEl/eFMrHhwADlJZG0wJOR8CEw8WUhc+QxYeBVUPCwsUEQQAdgsCXwcAFkUoWAc3VhMPCQFRQg8KOxkGQQtL'
cipher = b2a_hex(b64decode(cipher))
c = cipher.decode(encoding='utf-8')
a = []
for i in range(len(c)//2):
a.append(int(c[i*2],0x10)*0x10+int(c[i*2+1],0x10))
m = ''.join([chr(i) for i in a])
with open("task_r.txt", "w") as output_file:
output_file.write(m)

with open("task_r.txt") as input_file:
data = bytes(input_file.read(), encoding = "utf8")
result = break_repeating_key_xor(data)
print("cnss{%s}" % result[1].decode())
#cnss{Vig3nere_1s_vuner4ble}

KUNKUN’s Oracle

由两个同余方程可以得到两个 $X,n|(X^2-a)$,那么 $n|gcd(X_1^2-a,X_2^2-a)$

不妨假设 $n=gcd(X_1^2-a,X_2^2-a)$ 求出 flag_one_int 发现每次结果相同,flag_onecnss{Gu3ss_n_1s_s0_e4sy}

模数为 $n=pq$ 的二次同余方程有 $4$ 解或无解。

为简化此问题,不如考虑 $a=1$ 的情况:

对 $$X^2\equiv 1(\mod n)\tag{1}$$ 显然有 $1,n-1$ 两解,且剩余两解在模意义下互为相反数。

考虑方程
$$
X^2\equiv1(\mod p)\tag{2}\
$$
$$
X^2\equiv1(\mod q)\tag{3}
$$

对于 $(1)$ 的任意一个非平凡解 $X$ 都满足 $(2),(3)$,即有 $p|X\pm1,q|X\mp1$,那么 $\gcd(X-1,n)\neq1$,由此可得 $p$ 或 $q$ .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from random import randint
from hashlib import sha256
from itertools import product
from functools import reduce
from operator import add
from base64 import b64decode
from binascii import a2b_hex
from pwn import *
from math import gcd
from gmpy2 import invert as inv

sh = remote("47.100.39.140",2333)
msg1 = str(sh.recv(), encoding = "utf8")

sh.recv()
ch = [chr(i) for i in list(range(48,58))+list(range(65,91))+list(range(97,123))]
c1 = msg1[16:32]
c2 = msg1[37:-1]
for s in product(ch, repeat = 4):
if(sha256(bytes(reduce(add, s) + c1, encoding = "utf8")).hexdigest() == c2):
sh.sendline(reduce(add, s))
break

sh.recv()
msg2=sh.recv()
flag_one_int = 2438052038959711679742325770590891988999025531859378600317
xor_res = int(msg2[msg2.find(b'[*] (n ^ flag_one_int):') + 23 : msg2.find(b'[*] Secret is an unknown random string, e = 65537.') - 2])
c = int(msg2[msg2.find(b'[*] pow(secret_int, e, n):') + 26 : msg2.find(b'[*] Give me secret, then I will tell you flag_two.') - 1])
n = flag_one_int ^ xor_res

def Solve_the_equation():
sh.sendline("S")
sh.recv()
sh.recv()

sh.sendline("0x1")
sh.recv()
msg3=sh.recv()

return int(msg3[: msg3.find(b'[*] Options:') - 1])

X = Solve_the_equation()
if X == 1 or X == n - 1:
X = Solve_the_equation()

e = 65537
p = gcd(X + 1, n)
q = n // p

d = inv(e, (p - 1) * (q - 1))
m = pow(c, d, n)
s = str(a2b_hex(hex(m)[2:]), encoding="utf8")

sh.sendline("G")
sh.recv()

sh.sendline(s)
print(str(sh.recv(), encoding="utf8"), end='')
#cnss{Prim3_factoriz4tion_1s_n0t_e4sy}

Linear Fucking Shit Register

经典的 LFSR 已知长度 $2m$ 加密数据流推出 mask 的问题

$2m$ 个位之间可列出 $m$ 元一次模 $2$ 意义下线性方程组,可借助矩阵求逆求解,但本题所给数据得出的系数矩阵是奇异的,assert附加了条件使得方程有唯一解,不过这种方法时间复杂度高且特殊情况不好处理。

依稀记得芜湖集训的时候听zzt说数学题不会就上BM解递推式乱搞

准确来说BM可以 $O(n^2)$ 求常系数线性递推式,正适合题设情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
from functools import reduce
from operator import xor

def seq2int(a):
res = 0
for i in a:
res <<= 1
res |= i
return res

def BM(S):
m = 0
N,f = 0,One
N0,f0 = -1,One
n = len(S)

while N < n:
if S[N] != f.dot(S[:N]):
if 2*m>N:
f = f + f0 * X**(N-N0)
else:
f0, f = f, f + f0 * X**(N-N0)
N0 = N
m = N+1-m
N += 1
return f[:-1]

class Poly(list): #binary polynomial
def __add__(self,other):
if len(self)<len(other):
return other + self

else:
k = len(self) - len(other)
new = [a^b for (a,b) in zip(self,([0]*k)+other)]
try:
while new[0] == 0:
new.pop(0)
except IndexError:
new = []

return Poly(new)

def dot(self,S):
m = len(self)
N = len(S)
C = self[:-1]
S = S[N-m+1:]
return dot(C,S)

def __mul__(self,other):
deg = len(self) + len(other) - 2
prod = [0]*(deg + 1)
for k,x in enumerate(self):
for j,y in enumerate(other):
prod[k+j] ^= (x&y)
return Poly(prod)

def __pow__(self,ex):
if ex==0:
return Poly([1])
else:
return self*(self**(ex-1))

def dot(x,y):
return reduce(xor,[a&b for (a,b) in zip(x,y)],0)

X = Poly([1,0])
One = Poly([1])

out_int = 0x85d1a4924ccc6cefb56a52d2
out_seq = [ord(c) - ord('0') for c in bin(out_int)[2:]]
mask = BM(out_seq)

def LFSR_inv(state, mask):
str=bin(state)[2:].zfill(48)
new=str[-1:]+str[:-1]
new=int(new,2)
i = (new & mask) & 0xffffffffffff
lastbit = 0
while i != 0:
lastbit ^= (i & 1)
i = i >> 1
return state >> 1 | lastbit << 47

ans = seq2int(out_seq[:48])
for _ in range(48):
ans = LFSR_inv(ans, seq2int(mask))

print("cnss{%s%s}" % (hex(ans)[2:], hex(seq2int(mask))[2:]))
#cnss{a11ceb0bffffffbeefc0ff3e}